package com.gitee.wsl.doc.prosemirror.state.command.mark

import com.gitee.wsl.doc.prosemirror.model.attr.Attrs
import com.gitee.wsl.doc.prosemirror.model.mark.MarkType
import com.gitee.wsl.doc.prosemirror.model.node.ext.markApplies
import com.gitee.wsl.doc.prosemirror.state.Command
import com.gitee.wsl.doc.prosemirror.state.selection.SelectionRange
import com.gitee.wsl.doc.prosemirror.state.selection.TextSelection
import com.gitee.wsl.doc.prosemirror.transform.ext.addMark
import com.gitee.wsl.doc.prosemirror.transform.ext.removeMark
import com.gitee.wsl.ext.base.ifValue
import com.gitee.wsl.ext.string.endSpaced
import com.gitee.wsl.ext.string.isSpaceString
import com.gitee.wsl.ext.string.startSpaced
import kotlin.math.max
import kotlin.math.min


/// Create a command function that toggles the given mark with the
/// given attributes. Will return `false` when the current selection
/// doesn't support that mark. This will remove the mark if any marks
/// of that type exist in the selection, or add it otherwise. If the
/// selection is empty, this applies to the [stored
/// marks](#state.EditorState.storedMarks) instead of a range of the
/// document.
//export function toggleMark(markType: MarkType, attrs: Attrs | null = null, options?: {
//    /// Controls whether, when part of the selected range has the mark
//    /// already and part doesn't, the mark is removed (`true`, the
//    /// default) or added (`false`).
//    removeWhenPresent?: boolean
//    /// When set to false, this will prevent the command from acting on
//    /// the content of inline nodes marked as
//    /// [atoms](#model.NodeSpec.atom) that are completely covered by a
//    /// selection range.
//    enterInlineAtoms?: boolean
//    /// By default, this command doesn't apply to leading and trailing
//    /// whitespace in the selection. Set this to `true` to change that.
//    includeWhitespace?: boolean
//}): Command {
//    let removeWhenPresent = (options && options.removeWhenPresent) !== false
//    let enterAtoms = (options && options.enterInlineAtoms) !== false
//    let dropSpace = !(options && options.includeWhitespace)
//    return function(state, dispatch) {
//        let {empty, $cursor, ranges} = state.selection as TextSelection
//        if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType, enterAtoms)) return false
//        if (dispatch) {
//            if ($cursor) {
//                if (markType.isInSet(state.storedMarks || $cursor.marks()))
//                    dispatch(state.tr.removeStoredMark(markType))
//                else
//                    dispatch(state.tr.addStoredMark(markType.create(attrs)))
//            } else {
//                let add, tr = state.tr
//                if (!enterAtoms) ranges = removeInlineAtoms(ranges)
//                if (removeWhenPresent) {
//                    add = !ranges.some(r => state.doc.rangeHasMark(r.$from.pos, r.$to.pos, markType))
//                } else {
//                    add = !ranges.every(r => {
//                        let missing = false
//                        tr.doc.nodesBetween(r.$from.pos, r.$to.pos, (node, pos, parent) => {
//                        if (missing) return false
//                        missing = !markType.isInSet(node.marks) && !!parent && parent.type.allowsMarkType(markType) &&
//                                !(node.isText && /^\s*$/.test(node.textBetween(Math.max(0, r.$from.pos - pos),
//                        Math.min(node.nodeSize, r.$to.pos - pos))))
//                    })
//                        return !missing
//                    })
//                }
//                for (let i = 0; i < ranges.length; i++) {
//                let {$from, $to} = ranges[i]
//                if (!add) {
//                    tr.removeMark($from.pos, $to.pos, markType)
//                } else {
//                    let from = $from.pos, to = $to.pos, start = $from.nodeAfter, end = $to.nodeBefore
//                    let spaceStart = dropSpace && start && start.isText ? /^\s*/.exec(start.text!)![0].length : 0
//                    let spaceEnd = dropSpace && end && end.isText ? /\s*$/.exec(end.text!)![0].length : 0
//                    if (from + spaceStart < to) { from += spaceStart; to -= spaceEnd }
//                    tr.addMark(from, to, markType.create(attrs))
//                }
//            }
//                dispatch(tr.scrollIntoView())
//            }
//        }
//        return true
//    }
//}

fun Command.Companion.toggleMark(markType: MarkType, attrs: Attrs? = null, options:ToggleMarkOptions = ToggleMarkOptions()): Command {
    val removeWhenPresent = ( options.removeWhenPresent) != false
    val enterAtoms = (options.enterInlineAtoms) != false
    val dropSpace = !(options.includeWhitespace)
    return Command() {state, dispatch->
        val selection = state.selection as TextSelection
        val empty = selection.isEmpty
        val cursor = selection._cursor
        var ranges = selection.ranges
        if ((empty && cursor == null) || !state.doc.markApplies( ranges, markType, enterAtoms)) return@Command false
        if (dispatch!=null) {
            if (cursor != null) {
                if (markType.isInSet(state.storedMarks ?: cursor.marks())!=null)
                    dispatch(state.tr.removeStoredMark(markType))
                else
                    dispatch(state.tr.addStoredMark(markType.create(attrs)))
            } else {
                val tr = state.tr
                var add = false
                if (!enterAtoms) ranges = removeInlineAtoms(ranges)
                if (removeWhenPresent) {
                    add = !ranges.all { r -> state.doc.rangeHasMark(r.from.pos, r.to.pos, markType)}
                } else {
                    add = !ranges.any{r ->
                        var missing = false
                        tr.doc.nodesBetween(r.from.pos, r.to.pos){node, pos, parent, index ->
                            if (missing) return@nodesBetween false
                            missing = markType.isInSet(node.marks)==null && parent!=null && parent.type.allowsMarkType(markType) &&
                                    !(node.isText && (
                                               node.textBetween(max(0, r.from.pos - pos), min(node.nodeSize, r.to.pos - pos))
                                            ).isSpaceString)
                            true
                        }
                        return@Command !missing
                    }
                }
                ranges.forEach {
                    val from = it.from
                    val to = it.to
                    if (!add) {
                        tr.removeMark(from.pos, to.pos, markType)
                    } else {
                        val start = from.nodeAfter
                        val end = to.nodeBefore
                        var from = from.pos
                        var to = to.pos
                        val spaceStart = (dropSpace && start!=null && start.isText).ifValue( start!!.text.startSpaced.length , 0)
                        val spaceEnd = (dropSpace && end!=null && end.isText).ifValue(  end!!.text.endSpaced.length , 0)
                        if (from + spaceStart < to) { from += spaceStart; to -= spaceEnd }
                        tr.addMark(from, to, markType.create(attrs))
                    }
                }
                dispatch(tr.scrollIntoView())
            }
        }
        true
    }
}

data class ToggleMarkOptions(
    /// Controls whether, when part of the selected range has the mark
    /// already and part doesn't, the mark is removed (`true`, the
    /// default) or added (`false`).
    val removeWhenPresent: Boolean = false,
    /// When set to false, this will prevent the command from acting on
    /// the content of inline nodes marked as
    /// [atoms](#model.NodeSpec.atom) that are completely covered by a
    /// selection range.
    val enterInlineAtoms: Boolean= false,
    /// By default, this command doesn't apply to leading and trailing
    /// whitespace in the selection. Set this to `true` to change that.
    val includeWhitespace: Boolean= false
)


fun removeInlineAtoms(ranges: List<SelectionRange>): List<SelectionRange> {
    val result = mutableListOf<SelectionRange>()
    ranges.forEach {
        var from = it.from
        val to = it.to
        from.doc.nodesBetween(from.pos, to.pos, f= FUN@{node, pos, parent, index ->
            if (node.isAtom && node.content.size > 0 && node.isInline && pos >= from.pos && (pos + node.nodeSize) <= to.pos) {
                if (pos + 1 > from.pos)
                    result.add(SelectionRange(from, from.doc.resolve(pos + 1)))
                from = from.doc.resolve(pos + 1 + node.content.size)
                return@FUN false
            }
            true
        })
        if (from.pos < to.pos) result.add( SelectionRange(from, to))
    }
    return result
//    for (let i = 0; i < ranges.length; i++) {
//        let {$from, $to} = ranges[i]
//        $from.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
//        if (node.isAtom && node.content.size && node.isInline && pos >= $from.pos && pos + node.nodeSize <= $to.pos) {
//            if (pos + 1 > $from.pos) result.push(new SelectionRange($from, $from.doc.resolve(pos + 1)))
//            $from = $from.doc.resolve(pos + 1 + node.content.size)
//            return false
//        }
//    })
//        if ($from.pos < $to.pos) result.push(new SelectionRange($from, $to))
//    }
//    return result
}